<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实时语音转写</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

        body {
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
            color: #fff;
            min-height: 100vh;
            padding: 20px;
        }

        .container {
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
        }

        header {
            text-align: center;
            margin-bottom: 30px;
            padding: 20px;
            background: rgba(0, 0, 0, 0.3);
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
        }

        h1 {
            font-size: 2.5rem;
            margin-bottom: 10px;
            background: linear-gradient(to right, #ff7e5f, #feb47b);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .subtitle {
            font-size: 1.2rem;
            opacity: 0.9;
            margin-top: 10px;
        }

        .card {
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            border-radius: 15px;
            padding: 25px;
            margin-bottom: 25px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            border: 1px solid rgba(255, 255, 255, 0.18);
        }

        .card-title {
            font-size: 1.5rem;
            margin-bottom: 20px;
            color: #ff7e5f;
            display: flex;
            align-items: center;
        }

        .card-title i {
            margin-right: 10px;
            font-size: 1.8rem;
        }

        .input-group {
            display: flex;
            margin-bottom: 20px;
        }

        input {
            flex: 1;
            padding: 15px;
            border: none;
            border-radius: 8px 0 0 8px;
            background: rgba(255, 255, 255, 0.15);
            color: white;
            font-size: 1rem;
        }

        input::placeholder {
            color: rgba(255, 255, 255, 0.7);
        }

        button {
            padding: 15px 25px;
            border: none;
            border-radius: 8px;
            background: linear-gradient(to right, #ff7e5f, #feb47b);
            color: white;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        button:hover {
            transform: translateY(-3px);
            box-shadow: 0 5px 15px rgba(255, 126, 95, 0.4);
        }

        button:disabled {
            background: rgba(255, 255, 255, 0.2);
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .btn-stop {
            background: linear-gradient(to right, #ff416c, #ff4b2b);
        }

        .status-container {
            display: flex;
            justify-content: space-around;
            margin: 25px 0;
            flex-wrap: wrap;
        }

        .status-item {
            text-align: center;
            padding: 15px;
            min-width: 180px;
            background: rgba(0, 0, 0, 0.2);
            border-radius: 10px;
            margin: 10px;
        }

        .status-label {
            font-size: 1rem;
            opacity: 0.8;
            margin-bottom: 5px;
        }

        .status-value {
            font-size: 1.3rem;
            font-weight: 600;
        }

        .connected {
            color: #4ade80;
        }

        .disconnected {
            color: #f87171;
        }

        .recording {
            color: #f87171;
            animation: pulse 1.5s infinite;
        }

        @keyframes pulse {
            0% { opacity: 1; }
            50% { opacity: 0.5; }
            100% { opacity: 1; }
        }

        .result-container {
            height: 200px;
            overflow-y: auto;
            padding: 20px;
            background: rgba(0, 0, 0, 0.2);
            border-radius: 10px;
            margin-top: 20px;
            font-size: 1.2rem;
            line-height: 1.6;
        }

        .history-container {
            max-height: 300px;
            overflow-y: auto;
            margin-top: 20px;
        }

        .history-item {
            padding: 15px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }

        .history-item:last-child {
            border-bottom: none;
        }

        .history-text {
            margin-bottom: 5px;
        }

        .history-time {
            font-size: 0.8rem;
            opacity: 0.7;
        }

        .action-buttons {
            display: flex;
            gap: 15px;
            margin-top: 20px;
        }

        .btn-secondary {
            background: rgba(255, 255, 255, 0.1);
        }

        .visualizer {
            height: 100px;
            margin: 20px 0;
            display: flex;
            align-items: flex-end;
            justify-content: center;
            gap: 2px;
        }

        .bar {
            width: 6px;
            background: linear-gradient(to top, #ff7e5f, #feb47b);
            border-radius: 3px 3px 0 0;
        }

        .error {
            color: #ff4b2b;
            margin-top: 15px;
            padding: 10px;
            background: rgba(255, 75, 43, 0.1);
            border-radius: 8px;
            display: none;
        }

        @media (max-width: 768px) {
            .input-group {
                flex-direction: column;
            }

            input {
                border-radius: 8px;
                margin-bottom: 10px;
            }

            button {
                width: 100%;
            }

            .status-container {
                flex-direction: column;
                align-items: center;
            }

            .action-buttons {
                flex-direction: column;
            }
        }

        .switch-label {
            display: inline-flex;
            align-items: center;
            font-size: 1rem;
            cursor: pointer;
            user-select: none;
        }
        .switch-label input[type="checkbox"] {
            display: none;
        }
        .switch-slider {
            width: 44px;
            height: 24px;
            background: #ccc;
            border-radius: 24px;
            position: relative;
            margin-right: 10px;
            transition: background 0.2s;
            box-sizing: border-box;
        }
        .switch-slider:before {
            content: "";
            position: absolute;
            left: 2px;
            top: 2px;
            width: 20px;
            height: 20px;
            background: #fff;
            border-radius: 50%;
            transition: transform 0.2s;
            box-shadow: 0 1px 4px rgba(0,0,0,0.2);
        }
        .switch-label input[type="checkbox"]:checked + .switch-slider {
            background: linear-gradient(90deg, #4ade80, #22d3ee);
        }
        .switch-label input[type="checkbox"]:checked + .switch-slider:before {
            transform: translateX(20px);
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>实时语音转写系统</h1>
            <p class="subtitle">使用WebSocket实现实时语音识别与文字转写</p>
        </header>

        <div class="card">
            <h2 class="card-title">
                <i>🔌</i> 服务器连接设置
            </h2>
            <div class="input-group">
                <input type="text" id="serverUrl"
                placeholder="输入WebSocket服务器地址（例如：ws://localhost:10096）"
                value="ws://localhost:10096">
                <button id="connectBtn">连接服务器</button>
                <button id="disconnectBtn" class="btn-secondary" disabled style="margin-left:10px;">断开连接</button>
            </div>
            <div style="margin-top: 10px;">
                <label class="switch-label">
                    <input type="checkbox" id="heartbeatSwitch" checked>
                    <span class="switch-slider"></span>
                    心跳机制
                </label>
            </div>
            <div class="error" id="connectionError"></div>
        </div>

        <div class="card">
            <h2 class="card-title">
                <i>🎙️</i> 录音控制
            </h2>
            <div class="status-container">
                <div class="status-item">
                    <div class="status-label">WebSocket状态</div>
                    <div class="status-value disconnected" id="wsStatus">未连接</div>
                </div>
                <div class="status-item">
                    <div class="status-label">录音状态</div>
                    <div class="status-value" id="recordingStatus">未开始</div>
                </div>
                <div class="status-item">
                    <div class="status-label">录音时长</div>
                    <div class="status-value" id="recordingTime">00:00</div>
                </div>
                <div class="status-item">
                    <div class="status-label">队列状态</div>
                    <div class="status-value" id="queueStatus">0/5000</div>
                </div>
            </div>

            <div class="visualizer" id="visualizer"></div>

            <div class="action-buttons">
                <button id="startBtn" disabled>开始录音</button>
                <button id="stopBtn" class="btn-stop" disabled>停止录音</button>
                <button id="uploadBtn" class="btn-secondary">模拟实时转写</button>
                <button id="offlineBtn" class="btn-secondary">离线转写</button>
                </div>
            <div class="error" id="recordingError"></div>
        </div>

        <div class="card">
            <h2 class="card-title">
                <i>📝</i> 实时转写结果
            </h2>
            <div class="result-container" id="resultContainer">
                <p>转写结果将实时显示在这里...</p>
            </div>
        </div>

        <div class="card">
            <h2 class="card-title">
                <i>📚</i> 历史记录
            </h2>
            <div class="history-container" id="historyContainer">
                <div class="history-item">
                    <div class="history-text">您好，欢迎使用实时语音转写系统</div>
                    <div class="history-time">示例 - 10:30:25</div>
                </div>
            </div>
            <div class="action-buttons">
                <button id="exportBtn" class="btn-secondary">导出结果</button>
                <button id="clearBtn" class="btn-secondary">清空历史</button>
            </div>
        </div>
    </div>

    <!-- 隐藏的文件上传输入框 -->
    <input type="file" id="audioFileInput" accept="audio/*" style="display: none;">

    <script>
        // 全局变量
        let ws;
        let recordingTimer;
        let recordingSeconds = 0;
        let isConnected = false;
        let isRecording = false;
        let pingInterval = null; // 全局心跳定时器

        // 转写结果处理变量
        let _finalText = '';      // 最终文本
        let _tempText = '';       // 临时文本
        let _currentSession = ''; // 当前会话标识

        // 音频文件处理变量
        let uploadedAudioBuffer = null;  // 上传的音频缓冲区
        let audioPlaybackTimer = null;   // 音频播放定时器
        let audioFileSendTimer = null;   // 音频文件发送定时器
        let audioPlaybackPosition = 0;   // 音频播放位置
        let isPlayingUploadedAudio = false; // 是否正在播放上传的音频

        // 音频处理参数（优化后的参数）
        const SEND_SIZE = 1920;           // 每次发送1920字节
        const SEND_INTERVAL_MS = 20;      // 每20毫秒发送一次（提高频率）
        const MAX_QUEUE_SIZE = 5000;      // 增加队列长度
        let audioDataQueue = [];
        let sendTimer;

        // DOM元素
        const serverUrlInput = document.getElementById('serverUrl');
        const connectBtn = document.getElementById('connectBtn');
        const disconnectBtn = document.getElementById('disconnectBtn');
        const startBtn = document.getElementById('startBtn');
        const stopBtn = document.getElementById('stopBtn');
        const uploadBtn = document.getElementById('uploadBtn');
        const offlineBtn = document.getElementById('offlineBtn');
        const audioFileInput = document.getElementById('audioFileInput');
        const exportBtn = document.getElementById('exportBtn');
        const clearBtn = document.getElementById('clearBtn');
        const wsStatus = document.getElementById('wsStatus');
        const recordingStatus = document.getElementById('recordingStatus');
        const recordingTime = document.getElementById('recordingTime');
        const queueStatus = document.getElementById('queueStatus');
        const resultContainer = document.getElementById('resultContainer');
        const historyContainer = document.getElementById('historyContainer');
        const connectionError = document.getElementById('connectionError');
        const recordingError = document.getElementById('recordingError');
        const visualizer = document.getElementById('visualizer');
        const heartbeatSwitch = document.getElementById('heartbeatSwitch');

        // 创建可视化音频条
        for (let i = 0; i < 50; i++) {
            const bar = document.createElement('div');
            bar.classList.add('bar');
            visualizer.appendChild(bar);
        }

        const bars = document.querySelectorAll('.bar');

        // 连接WebSocket服务器
        connectBtn.addEventListener('click', () => {
            const url = serverUrlInput.value.trim();
            if (!url) {
                showError(connectionError, '请输入有效的WebSocket服务器地址');
                return;
            }

            connectWebSocket(url);
        });

        // 断开WebSocket连接
        disconnectBtn.addEventListener('click', () => {
            if (ws && ws.readyState === WebSocket.OPEN) {
                ws.close();
                console.log('🔌 主动断开WebSocket连接');
            }
        });

        // 开始录音
        startBtn.addEventListener('click', startRecording);

        // 停止录音
        stopBtn.addEventListener('click', stopRecording);

        // 上传音频文件（模拟实时转写）
        uploadBtn.addEventListener('click', () => {
            audioFileInput.click();
        });

        // 离线转写按钮
        offlineBtn.addEventListener('click', () => {
            audioFileInput.setAttribute('data-offline', 'true');
            audioFileInput.click();
        });

        // 处理文件选择
        audioFileInput.addEventListener('change', function(event) {
            if (audioFileInput.getAttribute('data-offline') === 'true') {
                // 离线转写模式
                alert('该功能正在开发中敬请期待');
                audioFileInput.removeAttribute('data-offline');
                // 清空文件选择
                event.target.value = '';
                return;
            }
            handleAudioFileUpload(event);
        });

        // 导出结果
        exportBtn.addEventListener('click', exportResults);

        // 清空历史
        clearBtn.addEventListener('click', () => {
            if (confirm('确定要清空所有转写结果和历史记录吗？')) {
                // 清空转写结果
                _finalText = '';
                _tempText = '';
                _currentSession = '';

                // 清空历史记录
                historyContainer.innerHTML = '';

                // 清空显示
                resultContainer.innerHTML = '<p>转写结果将实时显示在这里...</p>';

                console.log('已清空所有转写结果和历史记录');
            }
        });

        // 连接WebSocket
        function connectWebSocket(url) {
            if (ws) {
                ws.close();
            }

            showError(connectionError, '');
            wsStatus.textContent = '连接中...';
            wsStatus.className = 'status-value';

            // 验证URL格式
            if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
                showError(connectionError, '无效的WebSocket地址，请使用 ws:// 或 wss:// 开头');
                wsStatus.textContent = '连接失败';
                wsStatus.className = 'status-value disconnected';
                return;
            }

            try {

                // FastAPI 不支持子协议
                ws = new WebSocket(url, ["binary"]);

                // 使用默认的WebSocket构造函数
                // ws = new WebSocket(url);

                // === 新增：心跳保活机制 ===
                // let pingInterval = null;3

                ws.onopen = () => {
                    isConnected = true;
                    wsStatus.textContent = '已连接';
                    wsStatus.className = 'status-value connected';
                    startBtn.disabled = false;
                    console.log('✅ WebSocket连接成功');
                    updateButtons();
                    // 连接建立时不启动心跳，等录音/播放结束后再启动
                };

                ws.onmessage = (event) => {
                    try {
                        const data = JSON.parse(event.data);
                        console.log('📥 收到服务器响应:', data);

                        // 处理转写结果
                        processTranscriptionResult(data);

                    } catch (error) {
                        console.error('解析消息失败:', error);
                        console.log('原始数据:', event.data);
                        displayError('数据解析失败: ' + error.message);
                    }
                };

                ws.onerror = (error) => {
                    console.error('WebSocket错误:', error);
                    showError(connectionError, '连接错误: 请检查服务器是否启动，地址是否正确');
                };

                ws.onclose = (event) => {
                    isConnected = false;
                    wsStatus.textContent = '未连接';
                    wsStatus.className = 'status-value disconnected';
                    console.log('❌ WebSocket连接关闭:', event.code, event.reason);
                    updateButtons();
                    // 断开连接时清理心跳定时器
                    if (pingInterval) {
                        clearInterval(pingInterval);
                        pingInterval = null;
                        console.log('WebSocket断开，关闭心跳 -', new Date().toLocaleString());
                    }
                    if (isRecording) {
                        stopRecording();
                        showError(recordingError, '服务器连接已断开，录音已停止');
                    }
                };
            } catch (error) {
                console.error('创建WebSocket失败:', error);
                showError(connectionError, '创建连接失败: ' + error.message);
                wsStatus.textContent = '连接失败';
                wsStatus.className = 'status-value disconnected';
            }
        }

        // 处理音频文件上传
        async function handleAudioFileUpload(event) {
            const file = event.target.files[0];
            if (!file) return;

            try {
                console.log('处理音频文件:', file.name, '大小:', file.size, '字节');

                // 读取音频文件 - 使用原始采样率，不强制转换
                const arrayBuffer = await file.arrayBuffer();
                const audioContext = new AudioContext(); // 不强制设置采样率
                uploadedAudioBuffer = await audioContext.decodeAudioData(arrayBuffer);

                console.log('音频文件解码成功:', {
                    采样率: uploadedAudioBuffer.sampleRate,
                    声道数: uploadedAudioBuffer.numberOfChannels,
                    时长: uploadedAudioBuffer.duration,
                    总样本数: uploadedAudioBuffer.length
                });

                // 检查音频格式是否支持
                if (uploadedAudioBuffer.numberOfChannels > 1) {
                    console.warn('多声道音频将转换为单声道');
                }

                // 启用开始按钮
                startBtn.disabled = false;
                startBtn.textContent = '播放音频文件';

                // 清空文件选择
                event.target.value = '';

                showError(recordingError, '');

            } catch (error) {
                console.error('音频文件处理失败:', error);
                showError(recordingError, '音频文件处理失败: ' + error.message);
            }
        }

        // 开始录音或播放音频文件
        async function startRecording() {
            recordingError.style.display = 'none';
            // 录音开始时清理心跳定时器
            if (pingInterval) {
                clearInterval(pingInterval);
                pingInterval = null;
                console.log('录音开始，关闭WebSocket心跳');
            }

            // 检查是否有上传的音频文件
            if (uploadedAudioBuffer) {
                startAudioFilePlayback();
                return;
            }

            // 否则开始实时录音
            try {
                // 配置音频参数以匹配服务器要求
                const stream = await navigator.mediaDevices.getUserMedia({
                    audio: {
                        sampleRate: 16000,  // 16kHz采样率
                        channelCount: 1,    // 单声道
                        echoCancellation: true,
                        noiseSuppression: true,
                        autoGainControl: true
                    }
                });

                // 使用Web Audio API直接获取PCM数据
                const audioContext = new AudioContext({ sampleRate: 16000 });
                const source = audioContext.createMediaStreamSource(stream);

                audioDataQueue = [];

                // 直接使用ScriptProcessor（在本地环境中更稳定）
                const processor = audioContext.createScriptProcessor(2048, 1, 1);

                processor.onaudioprocess = (event) => {
                    if (!isRecording) return;

                    const inputData = event.inputBuffer.getChannelData(0);
                    const pcmData = new Int16Array(inputData.length);

                    // 转换为16位PCM
                    for (let i = 0; i < inputData.length; i++) {
                        pcmData[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768));
                    }

                    // 将PCM数据加入队列
                    const pcmArray = new Uint8Array(pcmData.buffer);
                    for (let i = 0; i < pcmArray.length; i++) {
                        audioDataQueue.push(pcmArray[i]);
                    }

                    // 检查队列大小并优化处理
                    if (audioDataQueue.length >= MAX_QUEUE_SIZE) {
                        // 保留最新的数据，丢弃旧数据
                        const excess = audioDataQueue.length - MAX_QUEUE_SIZE;
                        audioDataQueue.splice(0, excess);
                        console.warn(`队列已满，丢弃 ${excess} 字节旧数据`);
                    }

                    // 更新队列状态显示
                    queueStatus.textContent = `${audioDataQueue.length}/${MAX_QUEUE_SIZE}`;

                    // 启动发送定时器
                    startSendTimer();
                };

                // 连接音频节点
                source.connect(processor);
                processor.connect(audioContext.destination);

                // 保存引用以便停止时清理
                window.currentAudioContext = audioContext;
                window.currentProcessor = processor;
                window.currentSource = source;

                // 开始新的转写会话
                _currentSession = `session_${Date.now()}`;
                console.log('开始新的转写会话:', _currentSession);

                isRecording = true;
                recordingStatus.textContent = '录音中';
                recordingStatus.className = 'status-value recording';
                startBtn.disabled = true;
                stopBtn.disabled = false;

                // 发送开始录音的配置消息（与Dart文件保持一致）
                if (ws && ws.readyState === WebSocket.OPEN) {
                    const configMessage = {
                        mode: "2pass",
                        chunk_size: [5, 10, 5],
                        chunk_interval: 10,
                        wav_name: "default",
                        wav_format: "pcm",
                        is_speaking: true
                    };
                    ws.send(JSON.stringify(configMessage));
                    console.log('📤 发送录音配置:', configMessage);
                }

                // 开始计时
                recordingSeconds = 0;
                updateRecordingTime();
                recordingTimer = setInterval(updateRecordingTime, 1000);

                // 开始可视化
                visualizeAudio(stream);
            } catch (error) {
                console.error('录音失败:', error);
                showError(recordingError, '无法访问麦克风: ' + error.message);
                recordingStatus.textContent = '录音失败';
                recordingStatus.className = 'status-value';
            }

            updateButtons();
        }

        // 开始音频文件播放
        function startAudioFilePlayback() {
            if (!uploadedAudioBuffer) {
                console.error('没有上传的音频文件');
                return;
            }
            // 播放开始时清理心跳定时器
            if (pingInterval) {
                clearInterval(pingInterval);
                pingInterval = null;
                console.log('音频播放开始，关闭WebSocket心跳');
            }

            console.log('开始播放音频文件');

            // 初始化播放状态
            audioPlaybackPosition = 0;
            audioDataQueue = [];
            isPlayingUploadedAudio = true;

            // 开始新的转写会话
            _currentSession = `session_${Date.now()}`;
            console.log('开始新的转写会话:', _currentSession);

            isRecording = true;
            recordingStatus.textContent = '播放音频文件中';
            recordingStatus.className = 'status-value recording';
            startBtn.disabled = true;
            stopBtn.disabled = false;

            // 发送开始录音的配置消息
            if (ws && ws.readyState === WebSocket.OPEN) {
                const configMessage = {
                    mode: "2pass",
                    chunk_size: [5, 10, 5],
                    chunk_interval: 10,
                    wav_name: "default",
                    wav_format: "pcm",
                    is_speaking: true,
                    // 添加实时处理标志，让服务器知道这是实时流
                    realtime: true
                };
                ws.send(JSON.stringify(configMessage));
                console.log('📤 发送音频播放配置:', configMessage);
            }

            // 开始计时
            recordingSeconds = 0;
            updateRecordingTime();
            recordingTimer = setInterval(updateRecordingTime, 1000);

            // 创建音频播放（可选，让用户听到声音）
            try {
                const audioContext = new AudioContext();
                const source = audioContext.createBufferSource();
                source.buffer = uploadedAudioBuffer;
                source.connect(audioContext.destination);
                source.start();

                // 保存引用以便停止时清理
                window.currentAudioSource = source;
                window.currentAudioContext = audioContext;

                console.log('音频播放已开始，用户应该能听到声音');
            } catch (error) {
                console.warn('音频播放失败（不影响转写）:', error);
            }

            // 立即开始第一次数据提取和发送
            extractAndSendAudioData();

            // 开始音频数据提取和发送 - 使用合适的定时器频率
            audioPlaybackTimer = setInterval(() => {
                extractAndSendAudioData();
            }, 30); // 每30ms触发一次，确保数据块足够大
        }

        // 从音频文件中提取并发送数据
        function extractAndSendAudioData() {
            if (!isPlayingUploadedAudio || !uploadedAudioBuffer) return;

            const sampleRate = uploadedAudioBuffer.sampleRate;
            const totalSamples = uploadedAudioBuffer.length;

            // 计算每30ms对应的样本数（基于原始采样率，确保数据块足够大）
            const samplesPerInterval = Math.floor(sampleRate * 30 / 1000);

            // 计算当前需要提取的样本范围
            const startSample = audioPlaybackPosition;
            const endSample = Math.min(startSample + samplesPerInterval, totalSamples);

            if (startSample >= totalSamples) {
                // 播放结束，但延迟发送结束信号，给服务器时间处理最后的数据
                console.log('音频文件播放完成，等待服务器处理...');
                setTimeout(() => {
                    stopRecording();
                }, 2000); // 等待2秒让服务器处理完最后的数据
                return;
            }

            // 提取音频数据 - 处理多声道和采样率转换
            let extractedSamples;
            if (uploadedAudioBuffer.numberOfChannels === 1) {
                // 单声道直接提取
                extractedSamples = uploadedAudioBuffer.getChannelData(0).slice(startSample, endSample);
            } else {
                // 多声道转换为单声道（取平均值）
                const channels = [];
                for (let i = 0; i < uploadedAudioBuffer.numberOfChannels; i++) {
                    channels.push(uploadedAudioBuffer.getChannelData(i).slice(startSample, endSample));
                }

                extractedSamples = new Float32Array(endSample - startSample);
                for (let i = 0; i < extractedSamples.length; i++) {
                    let sum = 0;
                    for (let j = 0; j < channels.length; j++) {
                        sum += channels[j][i];
                    }
                    extractedSamples[i] = sum / channels.length;
                }
            }

            // 如果采样率不是16kHz，进行重采样
            if (sampleRate !== 16000) {
                const resampledSamples = resampleAudio(extractedSamples, sampleRate, 16000);
                extractedSamples = resampledSamples;
            }

            // 转换为16位PCM - 使用正确的缩放因子
            const pcmData = new Int16Array(extractedSamples.length);
            for (let i = 0; i < extractedSamples.length; i++) {
                // 使用32767作为最大值，确保不溢出
                pcmData[i] = Math.max(-32767, Math.min(32767, Math.round(extractedSamples[i] * 32767)));
            }

            // 将PCM数据加入队列
            const pcmArray = new Uint8Array(pcmData.buffer);
            for (let i = 0; i < pcmArray.length; i++) {
                audioDataQueue.push(pcmArray[i]);
            }

            // 检查队列大小
            if (audioDataQueue.length >= MAX_QUEUE_SIZE) {
                const excess = audioDataQueue.length - MAX_QUEUE_SIZE;
                audioDataQueue.splice(0, excess);
                console.warn(`队列已满，丢弃 ${excess} 字节旧数据`);
            }

            // 更新队列状态显示
            queueStatus.textContent = `${audioDataQueue.length}/${MAX_QUEUE_SIZE}`;

            // 启动发送定时器
            startSendTimer();

            // 更新播放位置
            audioPlaybackPosition = endSample;

            // 更新播放进度（减少日志频率，避免刷屏）
            if (audioPlaybackPosition % (sampleRate * 0.5) < samplesPerInterval) { // 每0.5秒显示一次
                const progress = (audioPlaybackPosition / totalSamples * 100).toFixed(1);
                console.log(`音频播放进度: ${progress}% (${audioPlaybackPosition}/${totalSamples})`);
            }

            // 立即发送数据，模拟实时录音的事件驱动
            if (audioDataQueue.length > 0) {
                // 直接发送，不等待定时器
                sendQueuedData();
            }

            // 调试信息：显示数据提取状态
            if (audioPlaybackPosition % (sampleRate * 1) < samplesPerInterval) { // 每1秒显示一次
                console.log(`数据提取状态: 位置=${audioPlaybackPosition}, 队列=${audioDataQueue.length}, 样本数=${extractedSamples.length}, PCM数据=${pcmData.length}, 采样率=${sampleRate}Hz`);
            }
        }

        // 停止录音或音频播放
        function stopRecording() {
            if (isRecording) {
                isRecording = false;
                clearInterval(recordingTimer);
                stopSendTimer();

                // 停止音频播放定时器
                if (audioPlaybackTimer) {
                    clearInterval(audioPlaybackTimer);
                    audioPlaybackTimer = null;
                }

                // 停止音频文件发送定时器
                if (audioFileSendTimer) {
                    clearInterval(audioFileSendTimer);
                    audioFileSendTimer = null;
                }

                // 清理音频资源
                if (window.currentProcessor) {
                    window.currentProcessor.disconnect();
                    window.currentProcessor = null;
                }
                if (window.currentSource) {
                    window.currentSource.disconnect();
                    window.currentSource = null;
                }
                if (window.currentAudioSource) {
                    window.currentAudioSource.stop();
                    window.currentAudioSource = null;
                }
                if (window.currentAudioContext) {
                    window.currentAudioContext.close();
                    window.currentAudioContext = null;
                }

                recordingStatus.textContent = isPlayingUploadedAudio ? '音频播放已停止' : '已停止';
                recordingStatus.className = 'status-value';
                queueStatus.textContent = '0/5000';
                startBtn.disabled = false;
                stopBtn.disabled = true;

                // 重置音频播放状态
                if (isPlayingUploadedAudio) {
                    isPlayingUploadedAudio = false;
                    audioPlaybackPosition = 0;
                    startBtn.textContent = '播放音频文件';
                } else {
                    startBtn.textContent = '开始录音';
                }

                // 发送停止录音的配置消息
                if (ws && ws.readyState === WebSocket.OPEN) {
                    const stopMessage = {
                        is_speaking: false
                    };
                    ws.send(JSON.stringify(stopMessage));
                    console.log('📤 发送停止录音消息:', stopMessage);
                }
                // 启动心跳定时器（录音/播放结束后，且开关为开）
                if (heartbeatSwitch.checked && ws && ws.readyState === WebSocket.OPEN && !pingInterval) {
                    pingInterval = setInterval(() => {
                        if (ws.readyState === WebSocket.OPEN) {
                            ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
                            console.log('💓发送心跳ping -', new Date().toLocaleString());
                        }
                    }, 10000);
                    console.log('💗启动心跳机制 -', new Date().toLocaleString());
                } else if (!heartbeatSwitch.checked) {
                    console.log('❌心跳机制开关关闭，不启动心跳');
                }

                // 重置可视化
                bars.forEach(bar => {
                    bar.style.height = '2px';
                });
            }

            updateButtons();
        }



        // 启动发送定时器
        function startSendTimer() {
            if (!sendTimer) {
                sendTimer = setInterval(() => {
                    if (audioDataQueue.length > 0) {
                        sendQueuedData();
                    }
                }, SEND_INTERVAL_MS);

                // 音频文件播放时，立即发送一次数据
                if (isPlayingUploadedAudio && audioDataQueue.length > 0) {
                    sendQueuedData();
                }
            }

            // 音频文件播放时，更频繁地发送数据
            if (isPlayingUploadedAudio && !audioFileSendTimer) {
                audioFileSendTimer = setInterval(() => {
                    if (audioDataQueue.length > 0) {
                        sendQueuedData();
                    }
                }, 20); // 每20ms发送一次，更接近实时录音
            }
        }

        // 停止发送定时器
        function stopSendTimer() {
            if (sendTimer) {
                clearInterval(sendTimer);
                sendTimer = null;
            }
        }

        // 简单的音频重采样函数
        function resampleAudio(samples, fromSampleRate, toSampleRate) {
            if (fromSampleRate === toSampleRate) {
                return samples;
            }

            const ratio = fromSampleRate / toSampleRate;
            const newLength = Math.round(samples.length / ratio);
            const resampled = new Float32Array(newLength);

            for (let i = 0; i < newLength; i++) {
                const oldIndex = i * ratio;
                const index1 = Math.floor(oldIndex);
                const index2 = Math.min(index1 + 1, samples.length - 1);
                const fraction = oldIndex - index1;

                resampled[i] = samples[index1] * (1 - fraction) + samples[index2] * fraction;
            }

            return resampled;
        }

        // 发送队列中的数据
        function sendQueuedData() {
            if (audioDataQueue.length === 0 || !ws || ws.readyState !== WebSocket.OPEN) return;

            try {
                            // 音频文件播放时，确保数据块足够大
            let targetSize = SEND_SIZE;
            if (isPlayingUploadedAudio) {
                // 音频文件播放时，等待足够的数据再发送
                if (audioDataQueue.length >= SEND_SIZE) {
                    targetSize = SEND_SIZE;
                } else {
                    // 如果数据不足，等待更多数据
                    return;
                }
            }

                // 累积音频数据到指定大小
                const buffer = [];

                while (audioDataQueue.length > 0 && buffer.length < targetSize) {
                    buffer.push(audioDataQueue.shift());
                }

                // 如果累积的数据超过发送大小，将多余部分放回队列
                if (buffer.length > SEND_SIZE) {
                    const excess = buffer.splice(SEND_SIZE);
                    audioDataQueue.unshift(...excess);
                }

                // 发送数据
                if (buffer.length > 0) {
                    const audioData = new Uint8Array(buffer);
                    ws.send(audioData);
                    console.log('✅ 发送PCM音频数据:', audioData.length + ' bytes');

                    // 更新队列状态显示
                    queueStatus.textContent = `${audioDataQueue.length}/${MAX_QUEUE_SIZE}`;
                }

            } catch (error) {
                console.error('发送音频数据失败:', error);
            }
        }



        // 处理转写结果
        function processTranscriptionResult(data) {
            const text = data.text || '';
            const isFinal = data.is_final || false;
            const status = data.status || '';
            const mode = data.mode || '';
            const speakerName = data.speaker_info?.name || '';

            console.log(`处理转写结果: 模式=${mode}, 状态=${status}, 最终=${isFinal}, 说话人=${speakerName}`);

            if (!text) {
                console.log('文本为空，跳过处理');
                return;
            }

            // 根据模式处理文本
            switch (mode) {
                case '2pass-online':
                    handleOnlineMode(text, status, isFinal);
                    break;
                case '2pass-offline':
                    handleOfflineMode(text, status, isFinal, speakerName);
                    break;
                default:
                    handleUnknownMode(text);
                    break;
            }

            // 识别结束处理
            if (isFinal) {
                // 只有2pass-offline的final才清空临时缓存
                // 2pass-online的final只是表示这段在线识别结束，等待离线优化结果
                if (mode === '2pass-offline') {
                    _tempText = '';
                    console.log('识别结束 - 离线模式，清空临时文本');
                }
            }

            // 更新显示
            updateDisplay();
        }

        // 处理在线模式
        function handleOnlineMode(text, status, isFinal) {
            console.log('处理在线模式');
            // 在线模式：实时显示，但是临时的 - 追加文本
            _tempText += text;
            console.log('在线模式 - 临时文本追加:', _tempText);
        }

        // 处理离线模式
        function handleOfflineMode(text, status, isFinal, speakerName) {
            console.log('处理离线模式');
            // 离线模式：更准确，替换对应的临时结果
            let formattedText = '';
            if (speakerName) {
                formattedText = `${speakerName}:\n${text}\n`;
            } else {
                formattedText = `${text}\n`;
            }

            // 追加到最终文本
            _finalText += formattedText;

            // 清空临时文本，因为已经被离线结果替换
            _tempText = '';

            console.log('离线模式 - 最终文本更新，清空临时文本:', _finalText);

            // 实时写入最终确定的文本到文件
            writeToFileRealtime(text, speakerName, new Date().toLocaleString());
        }

        // 处理未知模式
        function handleUnknownMode(text) {
            console.warn('未知的mode值，直接追加到_finalText');
            _finalText += text;
        }

        // 更新显示
        function updateDisplay() {
            // 显示组合结果：最终文本 + 临时文本
            const combinedText = _finalText + _tempText;
            resultContainer.innerHTML = `<p>${combinedText.replace(/\n/g, '<br>')}</p>`;

            // 滚动到底部
            resultContainer.scrollTop = resultContainer.scrollHeight;

            console.log('更新显示 - 最终文本长度:', _finalText.length, '临时文本长度:', _tempText.length);
        }

        // 实时写入文件
        function writeToFileRealtime(text, speaker, timestamp) {
            // 这里可以实现文件写入逻辑
            // 由于浏览器安全限制，实际写入需要后端支持
            console.log('实时写入文件:', text, '说话人:', speaker, '时间:', timestamp);

            // 格式化显示文本
            let displayText = '';
            if (speaker) {
                displayText = `[${timestamp}][${speaker}] ${text}`;
            } else {
                displayText = `[${timestamp}] ${text}`;
            }

            // 添加到历史记录
            const historyItem = document.createElement('div');
            historyItem.className = 'history-item';
            historyItem.innerHTML = `
                <div class="history-text">${displayText.replace(/\n/g, '<br>')}</div>
                <div class="history-time">${timestamp}</div>
            `;

            historyContainer.prepend(historyItem);
        }

        // 显示错误
        function showError(element, message) {
            if (message) {
                element.textContent = message;
                element.style.display = 'block';
            } else {
                element.style.display = 'none';
            }
        }

        // 更新录音时间
        function updateRecordingTime() {
            recordingSeconds++;
            const minutes = Math.floor(recordingSeconds / 60).toString().padStart(2, '0');
            const seconds = (recordingSeconds % 60).toString().padStart(2, '0');
            recordingTime.textContent = `${minutes}:${seconds}`;
        }

        // 更新按钮状态
        function updateButtons() {
            startBtn.disabled = !isConnected || isRecording;
            stopBtn.disabled = !isConnected || !isRecording;
            connectBtn.disabled = isConnected;
            disconnectBtn.disabled = !isConnected;
            uploadBtn.disabled = isRecording;
            offlineBtn.disabled = isRecording; // 离线转写按钮也受录音状态影响
        }

        // 导出结果
        function exportResults() {
            // 导出完整的转写结果
            let content = '语音转写完整记录:\n\n';
            content += '=== 最终转写结果 ===\n';
            content += _finalText;

            if (_tempText) {
                content += '\n=== 临时转写结果 ===\n';
                content += _tempText;
            }

            content += '\n=== 历史记录 ===\n';
            const items = historyContainer.querySelectorAll('.history-text');
            items.forEach((item, index) => {
                content += `${index + 1}. ${item.textContent}\n`;
            });

            const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
            const url = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = url;
            a.download = `语音转写_${new Date().toISOString().slice(0, 10)}_${new Date().toLocaleTimeString().replace(/:/g, '-')}.txt`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);

            console.log('导出完成，文件大小:', blob.size, '字节');
        }

        // 音频可视化
        function visualizeAudio(stream) {
            const audioContext = new AudioContext();
            const analyser = audioContext.createAnalyser();
            const microphone = audioContext.createMediaStreamSource(stream);

            analyser.smoothingTimeConstant = 0.8;
            analyser.fftSize = 1024;

            microphone.connect(analyser);
            analyser.connect(audioContext.destination);

            // 使用requestAnimationFrame进行可视化更新
            function updateVisualization() {
                if (!isRecording) return;

                const array = new Uint8Array(analyser.frequencyBinCount);
                analyser.getByteFrequencyData(array);

                // 更新可视化
                for (let i = 0; i < bars.length; i++) {
                    const index = Math.floor(i * array.length / bars.length);
                    const value = array[index] / 255;
                    const height = Math.max(2, value * 100);
                    bars[i].style.height = `${height}px`;
                }

                requestAnimationFrame(updateVisualization);
            }

            updateVisualization();
        }

        // 初始化
        updateButtons();
    </script>
</body>
</html>
